﻿#region Copyright
/// <copyright>
/// Copyright (c) 2008 Ilia Shramko.
/// Released under the terms of the GNU General Public License.
/// </copyright>
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;

using Osh.Dvb;
using Osh.Iptv;
using Osh.Media;

namespace Osh.ViewRight
{
	internal class ChannelStreamProcessor : WorkerBase
	{
		private TransportStreamDemultiplexer _demux;
		private UdpTransportStream _channelStream;
		private ProgramAssociationTable _programAssociationTable;
		private ProgramMapTable _programMapTable;

		public ChannelStreamProcessor()
			: base("ViewRight Channel Stream Processor")
		{
		}

		public ProgramAssociationTable ProgramAssociationTable
		{
			get { return _programAssociationTable; }
		}

		public ProgramMapTable ProgramMapTable
		{
			get { return _programMapTable; }
		}

		public void TuneToChannel(IMedia media)
		{
			this.Stop();

			_programAssociationTable = null;
			_programMapTable = null;

			UdpTransportStream stream = _channelStream;
			_channelStream = null;
			if(stream != null)
				stream.Dispose();

			if(media == null)
				return;

			Uri url = media.Attributes.GetValue<Uri>(MediaAttributeNames.SourceUrl);

			IPAddress multicastAddress = IPAddress.Parse(url.Host);
			_channelStream = new UdpTransportStream(multicastAddress, url.Port, IPAddress.Any);

			_demux = new TransportStreamDemultiplexer(_channelStream);
			this.Start();
		}

		public PacketStream OpenStream(int pid, PacketStreamType streamType)
		{
			TransportStreamDemultiplexer demux = _demux;
			if(demux == null)
				throw new InvalidOperationException();

			return demux.OpenStream(pid, streamType);
		}

		protected override void DoWork()
		{
			try
			{
				ProgramAssociationTable pat;
				if(!ReadPat(out pat))
					return;

				_programAssociationTable = pat;
				OnPatLoaded();

				int? pmtPid = null;
				foreach(int key in pat.Programs.Keys)
				{
					if(key != 0)
					{
						pmtPid = pat.Programs[key];
						break;
					}
				}
				if(pmtPid == null)
					return;

				ProgramMapTable pmt;
				if(!ReadPmt(pmtPid.Value, out pmt))
					return;

				_programMapTable = pmt;
				OnPmtLoaded();
			}
			catch(Exception e)
			{
				base.OnError(e);
			}
			finally
			{
				this.StopEvent.Set();
			}
		}

		private bool ReadPat(out ProgramAssociationTable programAssociationTable)
		{
			ProgramAssociationTable pat = new ProgramAssociationTable();

			using(PacketBinaryReader reader = new PacketBinaryReader(
				_demux.OpenStream((int)ServiceInformationPids.PAT, PacketStreamType.PSI)))
			{
				int currentVersion = -1;
				int nextSectionNumber = 0;
				while(reader.MoveToNextPacket())
				{
					int tableId = reader.ReadByte();
					if(tableId != (int)ServiceInformationTableIds.ProgramAssociationSection) break;

					int sectionLength = reader.ReadUInt16BE() & 0xFFF;
					
					pat.TransportStreamId = reader.ReadUInt16BE();
					
					int versionAndCurrentNextIndicator = reader.ReadByte();
					int version = (versionAndCurrentNextIndicator >> 1) & 0x1F;

					// A 1 bit indicator, which when set to '1' indicates that the Program Association Table sent is currently applicable.
					// When the bit is set to '0', it indicates that the table sent is not yet applicable
					// and shall be the next table to become valid.
					int currentNextIndicator = (versionAndCurrentNextIndicator & 1);
					if(currentNextIndicator != 1)
						continue;

					int sectionNumber = reader.ReadByte();
					int lastSectionNumber = reader.ReadByte();

					if((currentVersion != -1 && version != currentVersion) || (sectionNumber != nextSectionNumber))
					{
						currentVersion = -1;
						nextSectionNumber = 0;
						pat.Programs.Clear();
						continue;
					}
					currentVersion = version;

					int numberOfPrograms = (sectionLength - 9) >> 2;
					for(int i = 0; i < numberOfPrograms; i++)
					{
						int programNumber = (int)reader.ReadUInt16BE();
						int pid = reader.ReadUInt16BE() & 0x1FFF;
						pat.Programs[programNumber] = pid;
					}

					nextSectionNumber++;
					if(nextSectionNumber >= lastSectionNumber)
					{
						programAssociationTable = pat;
						return true;
					}
				}
			}
			programAssociationTable = null;
			return false;
		}

		private bool ReadPmt(int pmtPid, out ProgramMapTable programMapTable)
		{
			ProgramMapTable pmt = null;

			using(PacketBinaryReader reader = new PacketBinaryReader(_demux.OpenStream(pmtPid, PacketStreamType.PSI)))
			{
				DescriptorReader descriptorReader = new DescriptorReader(reader.BaseStream);

				int currentVersion = -1;
				while(reader.MoveToNextPacket())
				{
					if(pmt == null)
						pmt = new ProgramMapTable();

					int tableId = reader.ReadByte();
					if(tableId != (int)ServiceInformationTableIds.ProgramMapSection) break;

					int sectionLength = reader.ReadUInt16BE() & 0xFFF;
					pmt.ProgramNumber = reader.ReadUInt16BE();

					int versionAndCurrentNextIndicator = reader.ReadByte();
					int version = (versionAndCurrentNextIndicator >> 1) & 0x1F;

					// A 1 bit indicator, which when set to '1' indicates that the Program Association Table sent is currently applicable.
					// When the bit is set to '0', it indicates that the table sent is not yet applicable
					// and shall be the next table to become valid.
					int currentNextIndicator = (versionAndCurrentNextIndicator & 1);
					if(currentNextIndicator != 1)
						continue;

					reader.SkipBytes(2); // skip section_number & last_section_number which shall be always 0.

					if(currentVersion != -1 && version != currentVersion)
					{
						currentVersion = -1;
						pmt = null;
						continue;
					}
					currentVersion = version;

					pmt.PcrPid = reader.ReadUInt16BE() & 0x1FFF;

					int programInfoLength = reader.ReadUInt16BE() & 0xFFF;
					reader.SkipBytes(programInfoLength);

					int programDescriptorsLength = (sectionLength - 9 - programInfoLength - 4);
					while(programDescriptorsLength > 0)
					{
						ProgramElementDefinition programElement = new ProgramElementDefinition();
						programElement.StreamType = reader.ReadByte();	// stream_type
						programElement.Pid = (ushort)(reader.ReadUInt16BE() & 0x1FFF);	// elementary_PID

						int esInfoLength = reader.ReadUInt16BE() & 0xFFF;	// ES_info_length
						if(esInfoLength > 0)
							programElement.Descriptors.AddRange(descriptorReader.ReadDescriptors(esInfoLength, false));

						pmt.ProgramElements.Add(programElement);

						programDescriptorsLength -= 5 + esInfoLength;
					}

					break;
				}
			}

			programMapTable = pmt;
			return true;
		}

		protected void OnPatLoaded()
		{
			if(this.PatLoaded != null)
				this.PatLoaded(this, EventArgs.Empty);
		}

		protected void OnPmtLoaded()
		{
			if(this.PmtLoaded != null)
				this.PmtLoaded(this, EventArgs.Empty);
		}

		public event EventHandler<EventArgs> PatLoaded;
		public event EventHandler<EventArgs> PmtLoaded;
	}
}
